<?php
/**
 * SqlMap对象
 * 
 * 主要的灵感来源是Apache iBATIS项目，具体请参见 http://ibatis.apache.org/
 * 
 * 注意：由于历史原因，sqlmap中的参数位置（用问号表示的参数）索引从0开始，这和标准的statement不一致（索引从1开始）。I-F的IDbStatement行为则是标准的。
 *
 * @version $Id: ISqlMap.php 317 2008-05-04 15:33:31Z iwind $
 * @package util.sqlmap2
 */
class ISqlMap extends IObject {
	/**
	 * 数据库驱动
	 * 
	 * @var IDbDriver
	 */
	private $driver;
	/**
	 * @var IElementParser
	 */
	private $parser;
	
	/**
	 * 智能映射表字段和对象字段（属性）
	 * 
	 * 将表中的 field_name 认为对象中的 fieldName
	 *
	 */
	const FIELD_SMART_MAPPING = 1;
	
	/**
	 * 构造器
	 *
	 * @param string $configXmlFile 配置文件.路径应为绝对路径或者相对于IApplication所在目录.
	 * @throws ISqlMapException
	 */
	public function __construct($configXmlFile) {
		ILogger::info("construct sqlmap from '{$configXmlFile}'");
		if (!is_file($configXmlFile)) {
			$file = IApplication::getRelativeFile($configXmlFile);
		}
		else {
			$file = new IFile($configXmlFile);
		}
		if (!$file->isFile() || !$file->exists()) {
			throw new ISqlMapException("can't find sqlmap config file '{$configXmlFile}'");
		}
		$this->parser = new IElementParser($file);
		
		//get connector
		$connectionSetting = $this->parser->getConnectionSetting();
		if (isset($connectionSetting["dsn"])) {
			$dsn = new IDsn($connectionSetting["dsn"]);
			$dsn->dumpTo($connectionSetting);
			$this->driver = $dsn->getDriver($connectionSetting);
			$this->driver->connect();
		}
		else {
			throw new ISqlMapException("'{$configXmlFile}' must define a dsn");
		}
	}
	
	/**
	 * 取得数据库驱动
	 *
	 * @return IDbDriver
	 * @since 0.2
	 */
	public function getDriver() {
		return $this->driver;
	}
	
	/**
	 * 取得XML分析器
	 *
	 * @return IElementParser
	 * @since 0.1
	 */
	public function getParser() {
		return $this->parser;
	}
	
	/**
	 * 初始化事务
	 *
	 * @return boolean
	 */
	public function beginTransaction() {
		return $this->driver->beginTransaction();
	}
	
	/**
	 * 提交当前事务
	 *
	 * @return boolean
	 */
	public function commit() {
		return $this->driver->commit();
	}
	
	/**
	 * 回滚当前事务
	 *
	 * @return boolean
	 */
	public function rollBack() {
		return $this->driver->rollBack();
	}
	
	/**
	 * 关闭数据库连接
	 *
	 */
	public function close() {
		$this->driver->close();
	}
	
	/**
	 * 取得语句对象
	 *
	 * @param string $sqlId 在配置文件中定义的sqlId.
	 * @param array $replaceMap 替换列表
	 * @return IDbStatement
	 */
	public function getStatement($sqlId, $replaceMap = null) {
		$element = $this->parser->getElement($sqlId);
		if ($element == null) {
			throw new ISqlMapException("can't find statement by sql id '{$sqlId}'");
		}
		$element->setDriver($this->driver);
		$element->setReplaceMap($replaceMap);
		return $element->toStatement();
	}
	
	/**
	 * 执行某个SQL ID对应的SQL语句
	 *
	 * @param string $sqlId SQL定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 */	
	public function query($sqlId, $parameterMap = array(), $replaceMap = null) {
		$this->filterParameterMap($sqlId, $parameterMap);
		$statement = $this->getStatement($sqlId, $replaceMap);
		$statement->bindParams($this->zeroToOne($parameterMap));
		$statement->execute();
	}
	
	/**
	 * 执行SQL语句
	 *
	 * @param string $sql SQL语句
	 * @param array $parameterMap 参数集
	 * @return IDbStatement
	 */
	public function execute($sql, $parameterMap = array()) {
		$statement = $this->driver->prepare($sql, $this->zeroToOne($parameterMap));
		$statement->execute();
		return $statement;
	}
	
	/**
	 * 执行并返回PDO对象
	 *
	 
	 * @param string $sql SQL语句
	 * @param array $parameterMap 参数集
	 * @since 0.0.3
	 * @return IDbStatement
	 * @deprecated 请使用execute方法代替
	 */
	public function executeAsPDO($sql, $parameterMap = array()) {
		return $this->execute($sql, $parameterMap);
	}
	
	/**
	 * 执行delete语句
	 * 
	 * 返回影响的行数
	 * 
	 * @param string $sqlId SQL定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return integer 
	 */
	public function delete($sqlId, $parameterMap = array(), $replaceMap = null) {
		$this->checkStatement($sqlId, "delete", __METHOD__);
		$this->filterParameterMap($sqlId, $parameterMap);
		$statement = $this->getStatement($sqlId, $replaceMap);
		$statement->bindParams($this->zeroToOne($parameterMap));
		$statement->execute();
		return $statement->rowCount();
	}
	
	/**
	 * 执行insert语句
	 * 
	 * 返回影响的行数
	 * 
	 * @param string $sqlId SQL定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return integer 
	 */	
	public function insert($sqlId, $parameterMap = array(), $replaceMap = null) {
		$this->checkStatement($sqlId, "insert", __METHOD__);
		$this->filterParameterMap($sqlId, $parameterMap);
		$statement = $this->getStatement($sqlId, $replaceMap);
		$statement->bindParams($this->zeroToOne($parameterMap));
		$statement->execute();
		return $statement->rowCount();
	}
	
	/**
	 * 取得最后插入的ID
	 *
	 * @param string $name the name of a sequence object 
	 * @return string
	 */
	public function getLastInsertId($name = null) {
		return $this->driver->lastInsertId($name);
	}
	
	/**
	 * 执行update语句
	 * 
	 * 返回影响的行数
	 * 
	 * @param string $sqlId SQL定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return integer 
	 */
	public function update($sqlId, $parameterMap = array(), $replaceMap = null) {
		$this->checkStatement($sqlId, "update", __METHOD__);
		$this->filterParameterMap($sqlId, $parameterMap);
		$statement = $this->getStatement($sqlId, $replaceMap);
		$statement->bindParams($this->zeroToOne($parameterMap));
		$statement->execute();
		return $statement->getRowCount();
	}
	
	/**
	 * 取出所有数据并放入数组中，其中每条数据也使用数组表示
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return array
	 */
	public function selectAsArray($sqlId, $parameterMap = array(), $replaceMap = null) {
		$this->checkStatement($sqlId, "select", __METHOD__);
		$this->filterParameterMap($sqlId, $parameterMap);
		$statement = $this->getStatement($sqlId, $replaceMap);
		$statement->bindParams($this->zeroToOne($parameterMap));
		$statement->execute();
		return $statement->fetchAll(IDb::FETCH_BOTH);
	}
	
	/**
	 * 取出某一列数组，并放入到数组中，其中每条数据也使用数组表示
	 *
	 * @param string $sqlId sql定义ID
	 * @param integer $columnIndex 列所在位置
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return array
	 */
	public function selectSingleColumnAsArray($sqlId, $columnIndex, $parameterMap = array(), $replaceMap = null) {
		$this->checkStatement($sqlId, "select", __METHOD__);
		$this->filterParameterMap($sqlId, $parameterMap);
		$columnIndexType = gettype($columnIndex);
		if ($columnIndexType != "integer") {
			throw new ISqlMapException("column index expect to be 'integer', but now '{$columnIndex}' is '{$columnIndexType}'");
		}
		$statement = $this->getStatement($sqlId, $replaceMap);
		$statement->bindParams($this->zeroToOne($parameterMap));
		$statement->execute();
		return $statement->fetchAll(IDb::FETCH_COLUMN, $columnIndex);
	}
	
	/**
	 * 取出所有数据并放入数组中，其中每条数据使用对象表示
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param integer $fieldMapping 字段映射方式，目前有 ISqlMap::FIELD_SMART_MAPPING
	 * @param array $replaceMap 替换列表
	 * @return array
	 */
	public function selectAsObject($sqlId, $parameterMap = array(), $fieldMapping = null, $replaceMap = null) {
		$this->checkStatement($sqlId, "select", __METHOD__);
		$this->filterParameterMap($sqlId, $parameterMap);
		$statement = $this->getStatement($sqlId, $replaceMap);
		$statement->bindParams($this->zeroToOne($parameterMap));
		$statement->execute();
		$resultArray = $statement->fetchAll(IDb::FETCH_OBJ);
		
		//看看是否返回对象
		$element = $this->parser->getElement($sqlId);
		if ($element instanceof ISelectElement) {
			$resultMap = $element->getResultMap();
			if ($resultMap != null) { // parameter map
				$resultClassName = $resultMap->getClassName();
				if ($resultClassName != null) {
					$objectArray = array();
					$columnMap = $resultMap->getColumnMap();
					foreach ($resultArray as $stdObject) {
						$resultObject = IObjectFactory::getObject($resultClassName);
						foreach ($stdObject as $name=>$value) {
							if (!isset($columnMap[$name])) { // map 中是否定义了对应的类的属性名
								continue;
							}
							$name = $columnMap[$name];
							$methodName = "set" . ucwords($name);
							if ($fieldMapping == self::FIELD_SMART_MAPPING) {
								$methodName = str_replace("_", "", $methodName);
							}
							if (!method_exists($resultObject, $methodName)) {
								throw new ISqlMapException("call to undefined method {$resultClassName}::{$methodName}() when parsing SQL '{$sqlId}' resultMap parameter.");
							}
							$resultObject->$methodName($value);	
						}
						$objectArray[] = $resultObject;
					}
					return $objectArray;
				}
			}
			
			//处理resultClass
			$resultClassName = $element->getResultClassName();
			if ($resultClassName != null) {
				$objectArray = array();
				foreach ($resultArray as $stdObject) {
					$resultObject = IObjectFactory::getObject($resultClassName);
					foreach ($stdObject as $name=>$value) {
						$methodName = "set" . ucwords($name);
						if ($fieldMapping == self::FIELD_SMART_MAPPING) {
							$methodName = str_replace("_", "", $methodName);
						}
						if (!method_exists($resultObject, $methodName)) {
							throw new ISqlMapException("call to undefined method {$resultClassName}::{$methodName}() when parsing SQL '{$sqlId}' resultClass parameter.");
						}
						$resultObject->$methodName($value);	
					}
					$objectArray[] = $resultObject;
				}
				return $objectArray;
			}
		}
		
		return $resultArray;
	}
	
	/**
	 * 取出单条数据并放入数组中
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return array/null
	 */	
	public function selectSingleRowAsArray($sqlId, $parameterMap = array(), $replaceMap = null) {
		$array = $this->selectAsArray($sqlId, $parameterMap, $replaceMap);
		if (!empty($array)) {
			return $array[0];
		}
		return null;
	}
	
	
	
	/**
	 * 取出单行数据并放入数组中
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param integer $fieldMapping 字段映射方式，目前有 ISqlMap::FIELD_SMART_MAPPING
	 * @param array $replaceMap 替换列表
	 * @return array/null
	 */	
	public function selectSingleRowAsObject($sqlId, $parameterMap = array(), $fieldMapping = null, $replaceMap = null) {
		$array = $this->selectAsObject($sqlId, $parameterMap, $fieldMapping, $replaceMap);
		if (!empty($array)) {
			return $array[0];
		}
		return null;
	}
	
	/**
	 * 取出单行单列的值
	 * 
	 * 如果返回结果有多个值，则只返回第一个
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return mixed
	 */
	public function selectSingleValue($sqlId, $parameterMap = array(), $replaceMap = null) {
		$array = $this->selectAsArray($sqlId, $parameterMap, $replaceMap);
		if (isset($array[0][0])) {
			return $array[0][0];
		}
		return null;
	}
	
	/**
	 * 同selectAsArray
	 * 
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return array
	 * @see selectAsArray
	 */
	public function fetchAllArray($sqlId, $parameterMap = array(), $replaceMap = null) {
		return $this->selectAsArray($sqlId, $parameterMap, $replaceMap);
	}
	
	/**
	 * 同selectSingleColumnAsArray
	 *
	 * @param string $sqlId sql定义ID
	 * @param integer $columnIndex 列所在位置
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return array
	 * @see selectSingleColumnAsArray
	 */
	public function fetchColumnArray($sqlId, $columnIndex, $parameterMap = array(), $replaceMap = null) {
		return $this->selectSingleColumnAsArray($sqlId, $columnIndex, $parameterMap, $replaceMap);
	}
	
	/**
	 * 同selectAsObject
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param integer $fieldMapping 字段映射方式，目前有 ISqlMap::FIELD_SMART_MAPPING
	 * @param array $replaceMap 替换列表
	 * @return array
	 * @see selectAsObject
	 */
	public function fetchAllObject($sqlId, $parameterMap = array(), $fieldMapping = null, $replaceMap = null) {
		return $this->selectAsObject($sqlId, $parameterMap, $fieldMapping, $replaceMap);
	}
	
	/**
	 * 同selectSingleRowAsArray
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return array/null
	 * @see selectSingleRowAsArray
	 */	
	public function fetchArray($sqlId, $parameterMap = array(), $replaceMap = null) {
		return $this->selectSingleRowAsArray($sqlId, $parameterMap, $replaceMap);
	}
	
	
	
	/**
	 * 同selectSingleRowAsObject
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param integer $fieldMapping 字段映射方式，目前有 ISqlMap::FIELD_SMART_MAPPING
	 * @param array $replaceMap 替换列表
	 * @return array/null
	 * @see selectSingleRowAsObject
	 */	
	public function fetchObject($sqlId, $parameterMap = array(), $fieldMapping = null, $replaceMap = null) {
		return $this->selectSingleRowAsObject($sqlId, $parameterMap, $fieldMapping, $replaceMap);
	}
	
	/**
	 * 同selectSingleValue
	 *
	 * @param string $sqlId sql定义ID
	 * @param mixed $parameterMap 参数集(数组，对象，或者标量)
	 * @param array $replaceMap 替换列表
	 * @return mixed
	 * @see selectSingleValue
	 */
	public function fetchValue($sqlId, $parameterMap = array(), $replaceMap = null) {
		return $this->selectSingleValue($sqlId, $parameterMap, $replaceMap);
	}
	
	
	/**
	 * 检查SQL定义ID和元素标签是否一致
	 *
	 * @param string $sqlId SQL定义ID
	 * @param string $tagName 期望的标签名
	 * @param string $function 调用此方法的方法名
	 * @return boolean
	 * @throws ISqlMapException
	 */
	private function checkStatement($sqlId, $tagName, $method) {
		$element = $this->parser->getElement($sqlId);
		if ($element != null) {
			if (strtolower($element->getTagName()) !== strtolower($tagName)) {
				throw new ISqlMapException($element->getTagName() . " statement '{$sqlId}' can't be executed by '{$method}'");
			}
		}
		return true;
	}
	
	/**
	 * 过滤parameterMap
	 *
	 * @param unknown_type $sqlId
	 * @param Object $parameterMap
	 */
	private function filterParameterMap($sqlId, &$parameterMap) {
		$element = $this->parser->getElement($sqlId);
		if ($element == null) {
			throw new ISqlMapException("can't find statement by sql id '{$sqlId}'");
		}
		
		//占位参数
		$placeHolders = $element->getPlaceHolder();
		$namedParams = $placeHolders["namedParams"];
		$positionalParams = $placeHolders["positionalParams"];
		if (empty($namedParams) && empty($positionalParams)) {
			$parameterMap = array();//不需要任何参数,maybe need a info log
			return;
		}
		
		if (!empty($namedParams) && !empty($positionalParams)) {//不能同时使用
			throw new ISqlMapException("can't use both named params and positional params in one SQL '{$sqlId}'");
		}
		$bindType = (!empty($namedParams))?"named":"positional";
		
		$parameterMapElement = $element->getParameterMapElement();
		if ($parameterMapElement != null) {
			if ($bindType == "positional") {
				throw new ISqlMapException("can't use object on positional params in SQL '{$sqlId}'");
			}
			$className = $parameterMapElement->getClassName(); 
			$parameterObject = $parameterMap;
			if (!is_object($parameterObject)) {
				throw new ISqlMapException("SQL '{$sqlId}' parameterMap should be an object.");
			}
			if (!$parameterObject instanceof $className) {
				throw new ISqlMapException("SQL '{$sqlId}' parameterMap should be an '{$className}' instance.");
			}
			$parameterMap = array();
			$list = $parameterMapElement->getParameterList();
			$sqlId = $parameterMapElement->getSqlId();
			foreach ($list as $property) {
				if (isset($namedParams[$property])) {
					$method = "get" . ucfirst($property);
					if (!method_exists($parameterObject, $method)) {
						throw new ISqlMapException("call to undefined method {$className}::{$method}() when parsing SQL '{$sqlId}' parameterMap parameter.");
					}
					$parameterMap[$property] = $parameterObject->$method();
				}
			}
		}
		else {
			if (is_scalar($parameterMap) || $parameterMap == null) {
				if ($bindType == "named") {
					$parameterNames = array_keys($namedParams);
					$parameterMap = array($parameterNames[0] => $parameterMap);
				}
				else {
					$parameterMap = array($parameterMap);
				}
			}
			else if (is_object($parameterMap)) {
				if ($bindType == "positional") {
					throw new ISqlMapException("can't use object on positional params in SQL '{$sqlId}'");
				}
				$parameterObject = $parameterMap;
				$parameterMap = array();
				foreach ($parameterObject as $propertyName=>$propertyValue) {
					if (is_scalar($propertyValue) || $propertyValue == null) {
						if (isset($namedParams[$propertyName])) {
							$parameterMap[$propertyName] = $propertyValue;
						}
					}
				}
			}
			else if (is_array($parameterMap)) {
				$parameterArray = $parameterMap;
				$parameterMap = array();
				if ($bindType == "positional") {
					$parameterMap = array_values($parameterArray);
				}
				else {
					foreach ($parameterArray as $key=>$value) {
						$_key = trim($key);
						if (strlen($_key) == 0) {
							continue;
						}
						$realKey = ($_key{0} == ":")?substr($_key, 1):$_key;
						if (isset($namedParams[$realKey])) {
							$parameterMap[$realKey] = $value;
						}	
					}
				}
			}
		}
		
		//检查参数是否足够
		$parameterMapCount = count($parameterMap);
		if ($bindType == "named" && $parameterMapCount < count($namedParams)) {
			$parameterMapNames = array_keys($parameterMap);
			$namedParamsNames = array_keys($namedParams);
			throw new ISqlMapException("SQL '{$sqlId}' need more parameters, such as '" . implode(", ", array_diff($namedParamsNames, $parameterMapNames)) . "'");
		}
		else if ($parameterMapCount < count($positionalParams)) {
			throw new ISqlMapException("SQL '{$sqlId}' need " . ($parameterMapCount - count($positionalParams)) . " more parameters");
		}
	}
	
	/**
	 * 取得数据库连接器
	 *
	 * @return IConnector
	 * @since 0.0.3
	 */
	public function getConnector() {
		return $this->connector;
	}
	
	/**
	 * 格式化SQL
	 *
	 * @param unknown_type $sql
	 * @return string
	 * @since 0.1
	 */
	private function format($sql) {
		$sql = str_replace(" ", "&nbsp;", $sql);
		$sql = str_replace("\t", "&nbsp;&nbsp;&nbsp;&nbsp;", $sql);
		$sql = preg_replace("/\r?\n/", "<br/>\n", $sql);
		return $sql;
	}
	
	/**
	 * 修改参数列表，使其索引从1开始
	 *
	 * @since 0.2
	 */
	private function zeroToOne($params) {
		$results = $params;
		if (is_array($params)) {
			foreach ($params as $key => $param) {
				if (gettype($key) == "integer") {
					$results[$key + 1] = $param;
				}
				else {
					$results[$key] = $param;
				}
			}
		}
		return $results;
	}
}

?>